// HttpResponse.java - Class that represents HTTP responses.
//
// Copyright (C) 1999-2002  Smart Software Consulting
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//
// Smart Software Consulting
// 1688 Silverwood Court
// Danville, CA  94526-3079
// USA
//
// http://www.smartsc.com
//

package com.smartsc.http;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Vector;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class
HttpResponse
implements HttpReasonPhrases, HttpHeaders, HttpServletResponse
{
	protected
	HttpResponse( HttpServer server, HttpRequest req, Socket socket)
	throws IOException
	{
		this.server = server;
		this.req    = req;
		this.socket = socket;

		sessionType = server.getSessionType();

		os = socket.getOutputStream();
	}

	public static
	int
	sendError(
		OutputStream out, int statusCode, String reasonPhrase)
	{
		StringBuffer sbContent = new StringBuffer();
		try
		{
			StringBuffer sbStatus = new StringBuffer();
			sbStatus.append( statusCode);
			sbStatus.append( " ");
			sbStatus.append( reasonPhrase);

			String contentType = CONTENT_TYPE + ": text/html";

			sbContent.append( "<html><head><title>");
			sbContent.append( sbStatus);
			sbContent.append( "</title></head><body><h1>");
			sbContent.append( sbStatus);
			sbContent.append( "</h1></body></html>");

			StringBuffer sbContentLength = new StringBuffer( CONTENT_LENGTH);
			sbContentLength.append( ": ");
			sbContentLength.append( sbContent.length());

			out.write( HttpServer.HTTP_VERSION.getBytes());
			out.write( ' ');
			out.write( sbStatus.toString().getBytes());
			out.write( CRLF.getBytes());
			out.write( contentType.getBytes());
			out.write( CRLF.getBytes());
			out.write( sbContentLength.toString().getBytes());
			out.write( CRLF.getBytes());
			out.write( CRLF.getBytes());
			out.write( sbContent.toString().getBytes());
			out.close();
		}
		catch( IOException ioe) {}

		return sbContent.length();
	}

	protected
	void
	sendHeader()
	{
		if( headerSent)
		{
			return;
		}

		try
		{
			StringBuffer sb = new StringBuffer( HttpServer.HTTP_VERSION);
			sb.append( " ");
			sb.append( statusCode);
			sb.append( " ");
			sb.append( reasonPhrase);
			sb.append( CRLF);

			// Add headers
			setDateHeader( DATE, System.currentTimeMillis());
			setHeader( SERVER, HttpServer.SERVER_INFO);
			setHeader( CONTENT_TYPE, type);
			if( contentLength >= 0)
			{
				setHeader( CONTENT_LENGTH, Integer.toString( contentLength));
			}

			// If using cookies for session management
			if( sessionType == HttpServer.SESSION_TYPE_COOKIE)
			{
				HttpSession session = req.getSession( false);
				// If session exists and is new
				if( session != null && session.isNew())
				{
					Cookie sessionCookie = new Cookie(
						server.getSessionName(), session.getId() );
					//sessionCookie.setPath( req.getContextPath() + "/" );
					addCookie( sessionCookie);
				}
			}

			Enumeration names = headers.keys();
			while( names.hasMoreElements())
			{
				String name  = (String)names.nextElement();
				Enumeration values = ((Vector)headers.get( name)).elements();
				while( values.hasMoreElements())
				{
					sb.append( name);
					sb.append( ": ");
					sb.append( values.nextElement());
					sb.append( CRLF);
				}
			}

			// Send cookies
			Enumeration cookieEnum = cookies.elements();
			while( cookieEnum.hasMoreElements())
			{
				Cookie cookie = (Cookie)cookieEnum.nextElement();
				sb.append( SET_COOKIE);
				sb.append( ": ");
				sb.append( cookieToString( cookie));
				sb.append( CRLF);
			}

			sb.append( CRLF);
			os.write( sb.toString().getBytes());
			os.flush();
		}
		catch( IOException ioe)
		{
			server.log( "Exception in sendHeader(): " + ioe);
		}

		headerSent = true;
	}

	// package access only
	void
	close()
	throws IOException
	{
		if( !closed )
		{
			closed = true;
			server.logTransfer( getTransferLogMessage());

			if( pw != null)
			{
				pw.close();
			}
			if( sos != null)
			{
				sos.close();
			}
			os.close();
		}
	}

	protected
	String
	getTransferLogMessage()
	{
		StringBuffer sb = new StringBuffer();
		// Client IP
		sb.append( socket.getInetAddress().getHostAddress());
		// ident user (not supported)
		sb.append( " - ");
		// Auth User
		String remoteUser = req.getRemoteUser();
		if( remoteUser != null)
		{
			sb.append( remoteUser);
		}
		else
		{
			sb.append( "-");
		}
		// " ["
		sb.append( " [");
		// Date
		sb.append( new Date().toString());
		// "] \""
		sb.append( "] \"");
		// Request
		String method   = req.getMethod();
		String path     = req.getVerbatimRequestPath();
		String protocol = req.getProtocol();
		if( method != null && path != null && protocol != null)
		{
			sb.append( method );
			sb.append( " " );
			sb.append( path );
			sb.append( " " );
			sb.append( protocol );
		}
		else
		{
			sb.append( "-");
		}
		// "\" "
		sb.append( "\" ");
		// Status code
		sb.append( statusCode);
		// " "
		sb.append( " ");
		// Content-length
		if( contentLength > -1)
		{
			sb.append( contentLength);
		}
		else
		{
			sb.append( "-");
		}
		// " \""
		sb.append( " \"");
		// Referrer
		String referrer = req.getHeader( "Referer");
		if( referrer != null)
		{
			sb.append( referrer);
		}
		// "\" \""
		sb.append( "\" \"");
		// User agent
		String userAgent = req.getHeader( "User-Agent");
		if( userAgent != null)
		{
			sb.append( userAgent);
		}
		// "\""
		sb.append( "\"");

		return sb.toString();
	}

	protected static
	String
	cookieToString( Cookie cookie)
	{
		StringBuffer sb = new StringBuffer( cookie.getName());
		sb.append( "=");
		sb.append( cookie.getValue());

		// TODO Persistent Cookies (expires=)

		String path = cookie.getPath();
		if( path != null && !path.equals( ""))
		{
			sb.append( "; path=");
			sb.append( path);
		}

		String domain = cookie.getDomain();
		if( domain != null && !domain.equals( ""))
		{
			sb.append( "; domain=");
			sb.append( domain);
		}

		if( cookie.getSecure())
		{
			sb.append( "; secure");
		}

		return sb.toString();
	}

//	public static final byte[] CRLF = { 0x0d, 0x0a }; // "\r\n"
	public static final String CRLF = "\r\n";

	private HttpServer server;
	private HttpRequest req;
	private Socket socket;
	private int sessionType;
	private OutputStream os;
	private ServletOutputStream sos;
	private PrintWriter pw;

	private int statusCode = SC_OK;
	private String reasonPhrase = RP_OK;
	private int contentLength = -1;
	private String type = "text/plain";
	private Hashtable headers = new Hashtable();
	private Vector cookies = new Vector();

	protected boolean headerSent;
	protected boolean closed;

	// Flag indicating whether we including output from another servlet
	protected boolean including;
	public boolean isIncluding() { return including; }
	public void setIncluding( boolean including) { this.including = including; }

	public static final String[] days =
		{ "Sun, ", "Mon, ", "Tue, ", "Wed, ", "Thu, ", "Fri, ", "Sat, " };

	// From javax.servlet.ServletResponse
	public void flushBuffer()
	{
		sendHeader();
	}
	public int getBufferSize()
	{
		return 0;
	}
	public String getCharacterEncoding()
	{
		// TODO Proper handling of char encoding
		return System.getProperty( "file.encoding");
	}
	public Locale getLocale()
	{
		return Locale.getDefault();
	}
	public ServletOutputStream getOutputStream()
	throws IOException
	{
		if( pw != null)
			throw new IllegalStateException(
				"getWriter() already called." );

		sendHeader();

		if( sos == null)
			sos = new com.smartsc.http.ServletOutputStream(
				new BufferedOutputStream( os, server.getBufferSize()) );

		return sos;
	}
	public PrintWriter getWriter()
	throws IOException
	{
		if( sos != null)
			throw new IllegalStateException(
				"getOutputStream() already called." );

		if( pw == null)
			pw = new PrintWriter(
				new BufferedWriter(
					new OutputStreamWriter( os), server.getBufferSize() ) );

		sendHeader();
		return pw;
	}
	public boolean isCommitted()
	{
		return headerSent;
	}
	public void reset()
	{
		if( isIncluding() )
		{
			return;
		}

		if( headerSent)
			throw new IllegalStateException( "Header already sent.");

		statusCode = SC_OK;
		reasonPhrase = RP_OK;
		contentLength = -1;
		type = "text/plain";
		headers.clear();
		cookies.removeAllElements();
	}
	public void setBufferSize( int size)
	{
		/* Airball */
	}
	public void setContentLength(int contentLength)
	{
		if( !isIncluding() )
		{
			if( headerSent)
				throw new IllegalStateException( "Header already sent.");

			this.contentLength = contentLength;
		}
	}
	public void setContentType(String type)
	{
		if( !isIncluding() )
		{
			if( headerSent)
				throw new IllegalStateException( "Header already sent.");

			this.type = type;
		}
	}
	public void setLocale( Locale loc)
	{
		/* Airball */
	}

	// From javax.servlet.http.HttpServletResponse
	public void addCookie(Cookie cookie)
	{
		if( !isIncluding() )
		{
			cookies.addElement( cookie);
		}
	}
	public void addDateHeader(String name, long date)
	{
		Date d = new Date( date);
		StringBuffer sb = new StringBuffer( days[d.getDay()]);
		sb.append( d.toGMTString());
		addHeader( name, sb.toString());
	}
	public void addHeader(String name, String value)
	{
		if( name == null || name.length() == 0 || isIncluding() )
		{
			return;
		}

		if( value == null)
		{
			value = "";
		}

		Vector v = (Vector)headers.get( name);
		if( v == null)
		{
			v = new Vector();
			headers.put( name, v);
		}

		v.addElement( value);
	}
	public void addIntHeader(String name, int value)
	{
		addHeader( name, String.valueOf( value));
	}
	public boolean containsHeader(String name)
	{
		return headers.containsKey( name);
	}
	public String encodeRedirectURL( String url)
	{
		// TODO If going to different host or non-servlet,
		// do not encode sessionId.
		return encodeURL( url);
	}
	/** @deprecated */
	public String encodeRedirectUrl( String url)
	{
		return encodeRedirectURL( url);
	}
	public String encodeURL(String url)
	{
		// If not using url encoding
		if( sessionType != HttpServer.SESSION_TYPE_URL)
			return url;

		// If no session
		HttpSession session = req.getSession( false);
		if( session == null)
			return url;

		// Build session parameter string
		String sessionParam =
			"?" + server.getSessionName() + "=" +
			req.getSession().getId();

		// Look for first '?'
		int query = url.indexOf( '?');
		// If not found
		if( query == -1)
		{
			query = url.length();
		}

		StringBuffer encoded = new StringBuffer(
			url.substring( 0, query));

		encoded.append( sessionParam);

		// If url has characters after '?'
		if( query < url.length()-1)
		{
			encoded.append( "&");
			encoded.append( url.substring( query+1));
		}

		return encoded.toString();
	}
	/** @deprecated */
	public String encodeUrl(String url)
	{
		return encodeURL( url);
	}
	protected void removeHeader(String name)
	{
		if( !isIncluding() )
		{
			headers.remove( name);
		}
	}
	public void sendError(int sc)
	{
		// TODO What if isIncluding() == true?
		// TODO lookup reason phrase
		setStatus( sc);
		contentLength = sendError( os, sc, "Reason phrase not specified.");
	}
	public void sendError(int sc, String msg)
	{
		// TODO What if isIncluding() == true?
		setStatus( sc, msg);
		contentLength = sendError( os, sc, msg);
	}
	public void sendRedirect(String location)
	throws IOException
	{
		// TODO What if isIncluding() == true?
		if( headerSent)
			throw new IllegalStateException( "Header already sent.");

		setStatus( HttpStatusCodes.SC_FOUND, RP_TEMPORARY_REDIRECT);
		setHeader( CONTENT_TYPE, "text/html" );
		// TODO Make location absolute
		setHeader( LOCATION, location );

		StringBuffer sbContent = new StringBuffer( "<html><head>");
		sbContent.append( "<head>");
		sbContent.append( "<title>");
		sbContent.append( "Redirecting..." );
		sbContent.append( "</title></head><body><h1>");
		sbContent.append( "Redirecting..." );
		sbContent.append( "</h1>");
		sbContent.append( "The page you requested has moved to ");
		sbContent.append( "<a href=\"");
		sbContent.append( location);
		sbContent.append( "\">");
		sbContent.append( location);
		sbContent.append( "</a>");
		sbContent.append( "</body></html>");

		setHeader( CONTENT_LENGTH, Integer.toString( sbContent.length() ));

		sendHeader();
		os.write( sbContent.toString().getBytes());
		os.close();
	}
	public void setDateHeader(String name, long date)
	{
		removeHeader( name);
		addDateHeader( name, date);
	}
	public void setHeader(String name, String value)
	{
		removeHeader( name);
		addHeader( name, value);
	}
	public void setIntHeader(String name, int value)
	{
		removeHeader( name);
		addIntHeader( name, value);
	}
	public void setStatus(int sc)
	{
		if( !isIncluding() )
		{
			// TODO lookup reason phrase
			setStatus( sc, "Reason phrase not specified.");
		}
	}
	public void setStatus(int sc, String sm)
	{
		if( !isIncluding() )
		{
			if( headerSent)
				throw new IllegalStateException( "Header already sent.");

			this.statusCode = sc;
			this.reasonPhrase = sm;
		}
	}
}
